﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ChromaCode.Models;
using ChromaCode.Extensions;
using System.Windows.Documents;

namespace ChromaCode.Services
{
    /// <summary>
    /// Service for applying a set of character based rules to a range of text
    /// </summary>
    internal class CharRuleService : RuleBaseService<CharacterRule>
    {
        #region Public

        /// <summary>
        /// Initialise the charcter rule service and start highlighting the supplied paragraph
        /// </summary>
        /// <param name="rules">A list of rules to use</param>
        /// <param name="startParagraph">The paragraph to apply the highlighting to</param>
        public CharRuleService(List<Rule> rules, Paragraph startParagraph)
        {
            StartParagraph = startParagraph;

            // Get any character rules from the list of supplied rules
            Rules = rules.Where(x => x.TypeOfRule == RuleType.CharacterHighlighter).Select(y => (CharacterRule)y).ToList();

            // If we have rules and a text range then start the highlight process
            if ((Rules != null) && (Rules.Count > 0) && (StartParagraph != null))
            {
                DoHighlighting();
            }
        }

        #endregion Public

        #region Private

        /// <summary>
        /// Main function that carries out the highlight process
        /// </summary>
        private void DoHighlighting()
        {
            TextRange tr = new TextRange(StartParagraph.ContentStart, StartParagraph.ContentEnd);
            tr.ClearAllProperties();
            CharacterRule openRule = null;
            int startPos = 0;

            // Loop for ever and ever....
            for (; ; )
            {
                // Try and open a rule
                openRule = OpenRule(tr, ref startPos);

                if (openRule != null)
                {
                    if (!CloseRule(tr, ref startPos, openRule))
                    {
                        // We failed to close the rule so stop here
                        break;
                    }
                }
                else
                {
                    // We failed to open any rules so stop here
                    break;
                }
            }
        }

        /// <summary>
        /// Try and open a character rule
        /// </summary>
        /// <param name="range">The range that contains the text we are scanning</param>
        /// <param name="startPos">The position to start searching from, NB this is a ref parameter so it will be modified
        /// to point to the next location after the rule (if any) is found</param>
        /// <returns>A character rule or null if no rule is found</returns>
        private CharacterRule OpenRule(TextRange range, ref int startPos)
        {
            CharacterRule openedRule = null;
            string text = range.Text;

            for (int charIndex = startPos; charIndex < text.Length; charIndex++)
            {
                if (!char.IsLetterOrDigit(text[charIndex]))
                {
                    // We have a non letter character so see if we have a character rule to apply to it
                    openedRule = Rules.FindCharacterRule(text[charIndex]);

                    if (openedRule != null)
                    {
                        // We have found a rule so update the startPos variable and break out of the loop
                        startPos = charIndex;
                        break;
                    }
                }
            }

            return openedRule;
        }

        /// <summary>
        /// Try and close an open character rule
        /// </summary>
        /// <param name="range">The text range to scan from the start position to find where the character rule ends</param>
        /// <param name="startPos">The position to start scanning the text in the text range. NB this is a ref parameter
        /// so it will be modified to the next starting position after the rule is closed, or -1 if the rule is not closed</param>
        /// <param name="ruleToClose">The rule we want to try and close</param>
        /// <returns>True if the rule was closed successfully, otherwise false</returns>
        private bool CloseRule(TextRange range, ref int startPos, CharacterRule ruleToClose)
        {
            bool result = false;
            string text = range.Text;
            int endPos = -1;

            // Start scanning from the next character in the text on from the supplied start position
            for (int charIndex = startPos + 1; charIndex < text.Length; charIndex++)
            {
                if (char.IsLetter(text[charIndex]))
                {
                    // The next character is a letter so the rule can be closed at this point
                    endPos = charIndex;
                    result = true;
                    break;
                }

                if (!ruleToClose.Characters.Contains(text[charIndex]))
                {
                    // If we get here then the next character is not a letter (if failed the first test above) but
                    // it is not part of the current rule so we can close here
                    endPos = charIndex;
                    result = true;
                    break;
                }
            }

            // Create start and end pointers based on the start and end position
            TextPointer startPointer = range.Start.GetInsertationPointAtOffset(startPos);
            TextPointer endPointer = (endPos == -1) ? range.End : range.Start.GetInsertationPointAtOffset(endPos);

            // Using the text posinters create a highlight element
            HighlightElement element = new HighlightElement(startPointer, endPointer, ruleToClose);
            HighlightElements.Add(element);

            // Update the start position
            startPos = (endPos == -1) ? -1 : endPos;

            return result;
        }

        #endregion Private
    }
}
